2021.11.23 星期二 13:39
基础
介绍
呕心沥血,一文看懂 react hooks 中 useState、useEffect、useContext、useRef、自定义hook
React Hooks — useState 和 useEffect
class和函数式组件
API
useState
constructor: Function components don’t need a constructor. You can initialize the state in the useState call. If computing the initial state is expensive, you can pass a function to useState.
构造函数: 函数式组件不需要构造函数, 你可以在useState的调用中去初始化state, 如果计算state会有较大的开销, 那你可以传一个函数到useState中
useState惰性初始化函数和更新函数
- 惰性初始化函数在某些场景下可以规避性能问题,提升性能
- 更新函数默认参数可以确保每次访问的值是更新后的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/**
* 当函数组件更新re-render时,函数组件内所有代码都会重新执行一遍。此时initialState的初始值是一个相对开销较大的IO操作。每次函数组件re-render时,第一行代码都会被执行一次,引起不必要的性能损耗。
*/
const initialState = Number(window.localStorage.getItem('count'))
const initialState = () => Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(initialState)
/**
* # 更新函数
假设doSomethingAsync这个异步函数执行需要500ms,连续快速三次点击按钮,会发现最终count值为1,而不是我们想要的最新值3。
而在increment函数内部打印console.log可以发现,函数increment确实执行了三次,但是如果在setCount方法上方console.log(count)打印会发现count值一直是0。
目前我理解的原因是函数组件在一次re-render完成之前,我们连续三次点击按钮,调用increment方法时,setCount所访问的count值一直是未更新的值0导致的。
*/
function DelayedCounter() {
const [count, setCount] = React.useState(0)
const increment = async () => {
await doSomethingAsync()
setCount(count + 1)
}
return <button onClick={increment}>{count}</button>
}
function DelayedCounter() {
const [count, setCount] = React.useState(0)
const increment = async () => {
await doSomethingAsync()
setCount(previousCount => previousCount + 1)
}
return <button onClick={increment}>{count}</button>
}
props作为初始值
一定要注意useState的参数,它只在第一次渲染时起作用,给状态变量赋初始值,使组件拥有初始状态。在以后的渲染中,不管是调用更新函数导致的组件渲染,还是父组件渲染导致的它的渲染,参数都不会再使用了,直接被忽略了,组件中的state状态变量,获取的都是最新值。
如果你想像下面的代码一样,使用父组件每次传递过来的props 来更新state,就会有问题,因为props.message, 只会在第一次渲染中使用,以后组件的更新,它就会被忽略了。
接合useEffect(() => { setState() }, [props.state]);
useMemo和useCallback的Hook
在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。
子组件引用了number相关数据,但是当name相关数据发生变化,也会重绘整个组件,子组件虽然没有任何变化,也会重绘。为了避免不必要的子组件的重渲染,需要使用useMemo和useCallback的Hook。
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
useCallback 和 useMemo 参数相同,第一个参数是函数,第二个参数是依赖项的数组。主要区别是 React.useMemo 将调用 fn 函数并返回其结果,而 React.useCallback 将返回 fn 函数而不调用它。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function Child({ onButtonClick, data }) {
console.log('Child Render')
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
Child = memo(Child)
function App() {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello') // 表单的值
const addClick = useCallback(() => setNumber(number + 1), [number])
const data = useMemo(() => ({ number }), [number])
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
export default App;
useCallback
必要的时候使用。非必要使用简单函数。
useEffect
~每次数据更新,setState 会重新渲染页面。~
~如果调用多次,会触发多次页面~
useLayoutEffect
useEffect 的执行时机,useEffect和componnentDidMount都是在组件挂载后才执行。但对于componentDidMount来说,如果组件挂载后,你同步设置一个状态,React知道会触发另外一次渲染,它就不会使用第一次的渲染结果,而是将使用第二次渲染的结果,画在屏幕上。而useEffect则是在第一次渲染结果画到页面上后,才执行,再将第二次的渲染结果画到屏幕上,这样会造成闪动。如果不想要这种闪动,就要使用useLayoutEffect
问题
useCallback
useCallback 值不是最新的。
场景:
<!–
# 【react】hook中useState设置数据不能立即反映更改
useState设置方法不能立即反映更改
同样,这里的问题不仅是异步的性质,而且函数更新状态的当前闭包和状态更新使用状态值这一事实将放映在下一次重新渲染中,现有的闭包不会收到影响,而是会创建新的闭包。 现在,在当前状态下,挂钩中的值是由现有的闭包获得的,并且在重新渲染时,将根据是否再次创建函数来更新闭包。
即使添加了一个setTimeout函数,尽管超时将在重新渲染的一段时间后运行,但setTimeout仍将使用其先去关闭而不是更新后的值。
如果要对状态更新执行操作,则需要使用useEffect钩子,就像componentDidUpdate在类组件中使用一样,因为useState返回的setter没有回调模式
\$_PS: 虽然是state。但是感觉差不多
<!– End: #1 –!>
// 使用函数,而不是固定值,将最新值传给要处理的函数,并返回给这个设置值的函数
#2 react函数组件中使用useState改变值后立刻获取最新值
$_PS: 从源码,hooks和redux的角度 说明问题。
–>
需要配合userEffect,第二个数组参数,表示数组内的变量变化会执行userEffect的方法。